import { Helper } from "../core/helper";
import { LoopArray } from "../core/looparray";
import { types } from "../core/types";

const S0 = [
  0x01234567, 0x89abcdef
];
const Sm = [
  0x7311c281, 0x2425cfa0
];
const Q = [
  0x7311c281, 0x2425cfa0, 0x64322864, 0x34aac8e7, 0xb60450e9, 0xef68b7c1,
  0xe8fb2390, 0x8d9f06f1, 0xdd2e76cb, 0xa691e5bf, 0x0cd0d63b, 0x2c30bc41,
  0x1f8ccf68, 0x23058f8a, 0x54e5ed5b, 0x88e3775d, 0x4ad12aae, 0x0a6d6031,
  0x3e7f16bb, 0x88222e0d, 0x8af8671d, 0x3fb50c2c, 0x995ad117, 0x8bd25c31,
  0xc878c1dd, 0x04c4b633, 0x3b72066c, 0x7a1552ac, 0x0d6f3522, 0x631effcb
];
const Q_len = 30;
const MD6_T = [34, 36, 42, 62, 134, 178];
const MD6_RS = [10, 5, 13, 10, 11, 12, 2, 7, 14, 15, 7, 13, 11, 7, 6, 12];
const MD6_LS = [11, 24, 9, 16, 15, 9, 27, 15, 6, 2, 29, 8, 15, 5, 31, 9];

const MD6_BLOCK_SIZE = 512;
const MD6_KEY_SIZE = 128;
const HASH_LEN_SIZE = 32;
const MD6_CALC_SIZE = 128;
const MD6_RAW_SIZE = Q_len + 16 + 4 + MD6_CALC_SIZE;
class MD6_bag001 { public x0: number; public x1: number; }
class MD6_CTX {
  public r_buf: LoopArray<number>;
  public r: number;
  public total: number;
  public used: number;
  public buf: number[];
  public key: number[];
  public key_len: number;
  public size: number;
  public levels: number;
  public hash_buf: number[][];
  public hash_ix: number[];
  public hash_times: number[];
  public times: number;
  public get Levels(): number { return this.levels; }
  public get KeyLength(): number { return this.key_len; }
  public static PrepareKey(c: MD6_CTX, key: number[], len: number) {

    if (len < 0) return;
    if (len > MD6_KEY_SIZE) len = MD6_KEY_SIZE;
    let i, j;
    for (i = 0, j = 0; (i < 16) && ((j + 3) < len); i++, j += 4) {
      c.key[i] = Helper.uint((key[j] << 24) | (key[j + 1] << 16) | (key[j + 2] << 8) | key[j + 3]);
    }
    if (j < len) {
      c.key[i] = Helper.uint(key[j] << 24);
      if ((len & 0b11) != 0) {
        c.key[i] |= Helper.uint((key[j + 1] << 16));
        if ((len & 0b1) != 0) {
          c.key[i] |= Helper.uint(key[j + 2] << 8);
        }
      }
      i++;
    }
    c.key_len = i;
  }
  public SetKEY(key: number[], length: number) {
    if (key == null) return;
    MD6_CTX.PrepareKey(this, key, length);
  }
  public SetLevels(levels: number) {
    if (levels != 128 && levels != 256 && levels != 512) levels = 128;
    this.levels = Helper.uint(levels);
  }
  public SetSize(size: number) {

    this.size = Helper.uint(size);
    this.r = (40 + ((this.size / 4) >> 0));
    if (this.r < MD6_RAW_SIZE) {
      this.r_buf = LoopArray.New(MD6_RAW_SIZE);
    }
    else {
      this.r_buf = LoopArray.New(this.r);
    }
  }
}

export class MD6 {
  public static Init(): MD6_CTX {

    var c = new MD6_CTX();
    c.total = 0;
    c.used = 0;
    c.times = 0;
    c.size = 128;
    c.r = (40 + ((c.size / 4) >> 0));
    if (c.r < MD6_RAW_SIZE) {
      c.r_buf = LoopArray.New(MD6_RAW_SIZE);
    }
    else {
      c.r_buf = LoopArray.New(c.r);
    }
    c.levels = 64;
    c.key = new Array(16);
    c.key_len = 0;

    c.buf = new Array(512);
    c.hash_buf = new Array(c.levels);
    c.hash_ix = types.number_array(c.levels);
    c.hash_times = types.number_array(c.levels);
    return c;
  }
  public static X(bag: MD6_bag001, A: LoopArray<number>, pos: number) {
    bag.x0 ^= A.Get(pos);
    bag.x1 ^= A.Get(pos + 1);
  }
  public static A(bag: MD6_bag001, A: LoopArray<number>, pos_a: number, pos_b: number) {
    bag.x0 ^= (A.Get(pos_a) & A.Get(pos_b));
    bag.x1 ^= (A.Get(pos_a + 1) & A.Get(pos_b + 1));
  }
  public static R(bag: MD6_bag001, shift: number) {

    if (shift >= 32) {
      bag.x1 ^= Helper.uint(bag.x0 >>> (shift - 32));
      bag.x0 ^= 0;
    }
    else {
      bag.x1 ^= Helper.uint((bag.x0 << (32 - shift)) | (bag.x1 >>> shift));
      bag.x0 ^= Helper.uint(bag.x0 >>> shift);
    }
  }
  public static L(bag: MD6_bag001, shift: number) {

    if (shift >= 32) {
      bag.x0 ^= Helper.uint(bag.x1 << (shift - 32));
      bag.x1 ^= 0;
    }
    else {
      bag.x0 ^= Helper.uint(bag.x0 << shift) | (bag.x1 >>> (32 - shift));
      bag.x1 ^= Helper.uint(bag.x1 << shift);
    }
  }
  public static F(c: MD6_CTX) {
    let i, j, s, n;

    var s_0 = S0[0];
    var s_1 = S0[1];
    let bag = new MD6_bag001();
    let swap;
    for (j = 0, i = MD6_RAW_SIZE; j < (c.r << 1); j += 2, i += 32) {
      for (s = 0, n = 0; n < 16; n++, s += 2) {
        bag.x0 = s_0; bag.x1 = s_1;
        MD6.X(bag, c.r_buf, 0);
        MD6.X(bag, c.r_buf, 144);
        MD6.A(bag, c.r_buf, 142, 136);
        MD6.A(bag, c.r_buf, 116, 44);
        MD6.R(bag, MD6_RS[n]);
        MD6.L(bag, MD6_LS[n]);
        c.r_buf.PushOne(bag.x0 >>> 0);
        c.r_buf.PushOne(bag.x1 >>> 0);
      }

      swap = Helper.uint((s_0 & Sm[0]) ^ ((s_0 << 1) | (s_1 >>> 31) ^ 0));
      s_1 = Helper.uint((s_1 & Sm[1]) ^ ((s_1 << 1) ^ (s_0 >>> 31)));
      s_0 = swap;
    }
  }
  public static RecordHash(c: MD6_CTX, levels: number) {

    while (true) {
      let i = 1;
      if ((levels + 1) >= c.levels) {
        i = 0;
      }
      if (c.hash_buf[levels] == null) {
        c.hash_buf[levels] = new Array(MD6_CALC_SIZE);
        if (i == 0) c.hash_ix[levels] = HASH_LEN_SIZE;
      }
      c.r_buf.CopyTo(178 - HASH_LEN_SIZE, c.hash_buf[levels], c.hash_ix[levels], HASH_LEN_SIZE);
      if (c.hash_ix[levels] == 128 - HASH_LEN_SIZE) {
        MD6.PrepareRbuf(c, 0, 0, c.hash_times[levels], Helper.uint(levels + 1));
        c.r_buf.PushBytes(c.hash_buf[levels], 0, MD6_CALC_SIZE);
        MD6.F(c);

        c.hash_ix[levels] = 0;
        c.hash_times[levels]++;
        levels += i;
        continue;
      }
      else {
        c.hash_ix[levels] += HASH_LEN_SIZE;
      }
      break;
    }
  }
  public static CalcP(len: number): number {
    let p = Helper.uint((MD6_BLOCK_SIZE - len % MD6_BLOCK_SIZE) << 3);
    if (p == Helper.uint(MD6_BLOCK_SIZE << 3)) p = 0;
    return p;
  }
  public static PrepareRbuf(c: MD6_CTX, p: number, z: number, i: number, deep: number) {

    c.r_buf.PushBytes(Q, 0, Q_len);
    c.r_buf.PushBytes(c.key, 0, 16);
    c.r_buf.PushOne(Helper.uint(((deep + 1) & 0xff) << 24) | (Helper.uint(i / Math.pow(2, 32)) & 0xffffff));
    c.r_buf.PushOne(i & 0xffffffff);
    c.r_buf.PushOne(Helper.uint(((c.r & 0xfff) << 16) | ((c.levels & 0xff) << 8) | ((z & 0xf) << 4) | ((p & 0xf000) >> 12)));
    c.r_buf.PushOne(Helper.uint(((p & 0xfff) << 20) | ((c.key_len & 0xff) << 12) | (c.size & 0xfff)));
  }
  public static Mid(c: MD6_CTX, data: number[], pos: number, length: number) {

    let j, h, raw;
    for (j = 0, h = pos; j < (length >>> 2); j++, h += 4) {
      c.r_buf.PushOne(Helper.uint((data[h] << 24) | (data[h + 1] << 16) | (data[h + 2] << 8) | data[h + 3]));
    }
    if ((length & 0b11) != 0) {
      raw = Helper.uint(data[h] << 24);
      if ((length & 0b10) != 0) {
        raw |= Helper.uint(data[h + 1] << 16);
        if ((length & 0b1) != 0) {
          raw |= Helper.uint(data[h + 2] << 8);
        }
      }
      c.r_buf.PushOne(raw);
      h += 4;
    }
    if (MD6_CALC_SIZE > j) c.r_buf.Push(0, Helper.uint(MD6_CALC_SIZE - (h >>> 2)));
    MD6.F(c);
  }
  public static Update(c: MD6_CTX, data: number[], len: number) {
    let j;

    c.total += len;
    len += c.used;
    let offset = c.used;

    for (j = c.used; j < len; j += MD6_BLOCK_SIZE) {
      if (len - j <= MD6_BLOCK_SIZE) {
        Helper.ArrayCopy2(data, j- offset, c.buf, c.used, len -j);
        c.used += (len - j);
      }
      else {
        MD6.PrepareRbuf(c, 0, 0, c.times++, 0);
        if (c.used > 0) {
          if (MD6_BLOCK_SIZE != c.used)
          {
            Helper.ArrayCopy2(data, 0, c.buf, c.used, MD6_BLOCK_SIZE - c.used);
            j += MD6_BLOCK_SIZE - c.used;
          }
          j -= MD6_BLOCK_SIZE;
          c.used = 0;
          MD6.Mid(c, c.buf, 0, MD6_BLOCK_SIZE);
        }
        else {
          MD6.Mid(c, data, (j - offset), MD6_BLOCK_SIZE);
        }

        MD6.RecordHash(c, 0);
      }
    }
  }
  public static Final(c: MD6_CTX): number[] {
    let len = c.total;
    let p = MD6.CalcP(len);
    let z = 1;
    if (len > MD6_BLOCK_SIZE) z = 0;
    MD6.PrepareRbuf(c, p, z, c.times, 0);
    MD6.Mid(c, c.buf, 0, c.used);
    var hash = new Array(c.size >> 3);
    var levels = 0;
    while (true) {
      if (c.hash_ix[levels] == 0) {
        if ((levels + 1) >= c.levels || c.hash_buf[levels + 1] == null) {
          let j, k;
          for (k = 0, j = Helper.uint(MD6_RAW_SIZE - (c.size >>> 5)); k < Helper.uint(c.size >>> 5); k++, j++) {
            Helper.CopyFrom4Reverse(hash, c.r_buf.Get(j), k << 2);
          }
          break;
        }
      }
      c.r_buf.CopyTo(MD6_RAW_SIZE - HASH_LEN_SIZE, c.hash_buf[levels], c.hash_ix[levels], HASH_LEN_SIZE);
      len = Helper.uint((c.hash_times[levels]) << 9) + ((c.hash_ix[levels] + HASH_LEN_SIZE) << 2);
      p = MD6.CalcP(len);

      var i = 1;
      if ((levels + 1) >= c.levels) {
        i = 0;
        z = 1;
      }
      else {
        z = 1;
        if (len > MD6_BLOCK_SIZE) z = 0;
      }

      MD6.PrepareRbuf(c, p, z, c.hash_times[levels], (levels + 1));
      c.r_buf.PushBytes(c.hash_buf[levels], 0, c.hash_ix[levels] + HASH_LEN_SIZE);
      if (c.hash_ix[levels] != MD6_CALC_SIZE - HASH_LEN_SIZE) {
        c.r_buf.Push(0, MD6_CALC_SIZE - HASH_LEN_SIZE - c.hash_ix[levels]);
      }
      MD6.F(c);

      c.hash_ix[levels] = 0;
      levels += i;
    }

    return hash;
  }
  public static JustMake(data: number[]): string {
    let c = MD6.Init();
    MD6.Update(c, data, data.length);
    return Helper.ToHex(MD6.Final(c));
  }
}

export class MD6_512 {
  public _ctx: MD6_CTX;
  public New() {
    this._ctx = MD6.Init();
    this._ctx.SetSize(512);
  }
  public static JustMake(data: number[]): string {
    var c = MD6.Init();
    c.SetSize(512);
    MD6.Update(c, data, data.length);
    return Helper.ToHex(MD6.Final(c));
  }
  public Update(data: number[]) {
    MD6.Update(this._ctx, data, data.length);
  }
  public Make(): string {
    return Helper.ToHex(MD6.Final(this._ctx));
  }
}